<?PHP

/*
 * 协议
 *     包类型 包的检验和 包的数据
 *     1个字节  2个字节   1021
 *
 *
 *     包类型：
 *          连接包 PACKET_CONNECT
 *          连接回应包 PACKET_CONN_ACK
 *          错误包 PACKET_ERROR
 *          断开包 PACKET_DISCONNECT
 */

class udpException extends Exception
{
    public function __construct($msg = '', $code = 0)
    {
        parent::__construct($msg, $code);
    }

    public function __toString()
    {
        return 'udp('.$this->getCode().'): '.$this->getMessage();
    }
}

class udp
{
    const PACKET_MAX = 1024;

    const PACKET_ALL = 255;
    const PACKET_CONNECT = 0;
    const PACKET_CONN_ACK = 1;
    const PACKET_ERROR  = 2;
    const PACKET_DISCONNECT = 3;

    private $sd = null;
    private $addr = null;
    private $port = null;
    private $pid = array();

    public function __construct($addr, $port)
    {
        $this->sd = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
        if (!$this->sd)
            throw new udpException('创建套接字失败!', 1);
        $this->addr = $addr;
        $this->port = $port;
    }

    public function __get($name)
    {
        return $this->$name;
    }

    static public function sum($data)
    {
        $l = strlen($data);
        for ($i = 0, $l = strlen($data), $s = 1; $i < $l; ++$i) {
            $s += ord($data[$i]);
        }
        $s <<= 2;
        return $s % 65535;
    }

    static public function checkPacket($data, $type = udp::PACKET_ALL)
    {
        $packet = unpack("Ctype/Ssum/a*data", $data);
        // echo self::sum($packet['data']), PHP_EOL;
        // echo 'type: ',$packet['type'],', recv: ', $packet['sum'], ', sum = ', self::sum($packet['data']), PHP_EOL;
        if ($type != udp::PACKET_ALL && $packet['type'] != $type
                || $packet['sum'] != self::sum($packet['data'])) {
            // echo "check failed!\n";
            return FALSE;
        }

        if ($type == udp::PACKET_ALL) {
            unset($packet['sum']);
            return $packet;
        }
        return $packet['data'];
    }

    public function connect_handle($handle, & $data)
    {
        $cli = array();
        while (1) {
            $ret = socket_recvfrom($this->sd, $packet_data,
                    udp::PACKET_MAX, 0,
                    $cli['addr'], $cli['port']);
            if ($ret === FALSE)
                continue;

            $cli['conn'] = self::checkPacket($packet_data, udp::PACKET_CONNECT);
            if ($cli['conn'] === FALSE)
                continue;



            $cli['sd'] = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
            if ($cli['sd'] === FALSE)
                continue;

            socket_connect($cli['sd'], $cli['addr'], $cli['port']);

            call_user_func_array($handle, array($cli, $this, & $data));

            socket_close($cli['sd']);
        }
        return 0;
    }

    public function srv($handle, & $data = null, $max = 5)
    {
        if ($handle == null) {
            throw new udpException('客户处理函数不能为空!', 2);
        }

        if (socket_bind($this->sd, $this->addr, $this->port) === FALSE)
            throw new udpException('IP地址与端口绑定失败!', 3);

        for ($i = 0; $i < $max; ++$i) {
            $this->pid[$i] = pcntl_fork();
            if ($this->pid[$i] == 0) {
                exit($this->connect_handle($handle, $data));
            }
        }
    }

    public function __destruct()
    {
        socket_close($this->sd);
    }

    public function stop_srv()
    {
        foreach ($this->pid as $pid) {
            posix_kill($pid, SIGTERM);
        }
    }

    public function recv($sd = null, $type = udp::PACKET_ALL, $ms = 0, & $addr = null, & $port = 0)
    {
        if ($ms == 0) {
            $sec = null;
            $usec = 0;
        } else {
            $sec = (int)($ms / 1000);
            $usec = ($ms % 1000) * 1000;
        }

        if ($sd == null)
            $sd = $this->sd;

        $r = array($sd);
        $ret = socket_select($r, $w, $e, $sec, $usec);
        // var_dump($sec, $usec, $ret, $r, $sd);
        if ($ret > 0 && $r[0] === $sd) {
            // echo 'addr: ', $addr, PHP_EOL;
            if ($addr === null)
                $packet = socket_read($sd, udp::PACKET_MAX);
            else
                $ret = socket_recvfrom($sd, $packet, udp::PACKET_MAX, 0, $addr, $port);
            if ($ret === FALSE || $packet == FALSE)
                return FALSE;
            return self::checkPacket($packet, $type);
        }

        return FALSE;
    }

    public function send($sd = null, $type = udp::PACKET_ALL, $data = "", $addr = null, $port = 0)
    {
        if ($sd == null)
            $sd = $this->sd;
        $sum = self::sum((string)$data);
        // echo "send data_len: ", strlen($data),PHP_EOL;
        // echo "send data: ", $data, ", ", self::sum($data), PHP_EOL;
        $packet = pack('CSa*', $type, $sum, $data);
        $packet_len = strlen($packet);
        // echo "send sum: ", $sum, ", ";
        // echo 'send len: ', $packet_len, PHP_EOL;
        if ($addr == null)
            return socket_write($sd, $packet, $packet_len);
        return socket_sendto($sd, $packet, $packet_len, 0, $addr, $port);
    }

    public function cli($data = "")
    {
        $ret = $this->send($this->sd, udp::PACKET_CONNECT, $data, $this->addr, $this->port);
        if ($ret === FALSE)
            throw new udpException('连接服务器失败!', 4);

        $connect = $this->recv($this->sd, udp::PACKET_ALL, 10000, $this->addr, $this->port);
        // echo $connect, PHP_EOL;
        if ($connect) {
            $ret = socket_connect($this->sd, $this->addr, $this->port);
            // printf("connect %s, %d, %d\n", $this->addr, $this->port, $ret);
        }

        return $connect;
    }
}

